package form

import (
	"fmt"
	"gitee.com/gcom/gbox/tag"
	"reflect"
	"strings"
	"sync"
)

type StructMeta struct {
	// type of the struct
	Typ reflect.Type
	// alias added before field alias
	ParentAlias string
	// fields of type
	Fields []*FieldMeta
}

func (m *StructMeta) GetField(alias string) *FieldMeta {
	for _, field := range m.Fields {
		if strings.EqualFold(field.Alias, alias) {
			return field
		}
	}
	return nil
}

type FieldMeta struct {
	// type of the field
	Typ reflect.Type
	// field name
	Name string
	// alias for field name. if there isn't an alias, the alias value is same as field name
	Alias string
	// canonicalAlias is almost the same as the alias, but is prefixed with
	// an embedded struct field alias in dotted notation if this field is
	// promoted from the struct.
	// For instance, if the alias is "N" and this field is an embedded field
	// in a struct "X", canonicalAlias will be "X.N".
	CanonicalAlias string
	// is field anonymous
	Anonymous bool
	// field tag
	Tag tag.TagOptions
	// whether omit empty value, 1 for omit, -1 for not omit, 0 for undefined.
	OmitEmpty int8
}

type cache struct {
	l sync.RWMutex
	// map for type meta
	structs map[reflect.Type]*StructMeta
	// the tag name used to parse tag options
	tag string
}

func newCache(tag string) *cache {
	c := cache{
		structs: make(map[reflect.Type]*StructMeta),
		tag:     tag,
	}
	return &c
}

func (c *cache) Get(t reflect.Type) (*StructMeta, error) {
	c.l.RLock()
	info := c.structs[t]
	c.l.RUnlock()
	if info == nil {
		var err error
		info, err = c.registerType(t, "")
		if err != nil {
			return nil, err
		}
		c.l.Lock()
		c.structs[t] = info
		c.l.Unlock()
	}
	return info, nil
}

func (c *cache) registerType(t reflect.Type, parentAlias string) (*StructMeta, error) {
	if t.Kind() != reflect.Struct {
		return nil, fmt.Errorf("only struct type can be add the cache")
	}
	m := &StructMeta{
		Typ:         t,
		ParentAlias: parentAlias,
	}

	var anonymousInfos []*StructMeta
	for i := 0; i < t.NumField(); i++ {
		f, e := c.createField(t, t.Field(i), parentAlias)
		if e != nil {
			return nil, e
		}
		if f != nil {
			m.Fields = append(m.Fields, f)
			if ft := indirectPtr(f.Typ); ft.Kind() == reflect.Struct && f.Anonymous {
				af, err := c.registerType(ft, parentAlias)
				if err != nil {
					return nil, err
				}
				anonymousInfos = append(anonymousInfos, af)
			}
		}
	}
	for i, a := range anonymousInfos {
		others := []*StructMeta{m}
		others = append(others, anonymousInfos[:i]...)
		others = append(others, anonymousInfos[i+1:]...)
		for _, f := range a.Fields {
			if !containsAlias(others, f.Alias) {
				m.Fields = append(m.Fields, f)
			}
		}
	}
	return m, nil
}

func (c *cache) createField(t reflect.Type, f reflect.StructField, parentAlias string) (*FieldMeta, error) {
	tagOpt, err := tag.ParseTag(f.Tag.Get(c.tag), t.Name()+"."+f.Name)
	if err != nil {
		return nil, fmt.Errorf("parse struct field tag failed: %s.%s, %w", t.Name(), f.Name, err)
	}
	if ig, err := tagOpt.BoolAttr("ignore"); err == nil {
		if ig != nil && *ig {
			return nil, nil
		}
	} else {
		return nil, fmt.Errorf("invalid tag value of form.ignore, %s.%s: %w", t.Name(), f.Name, err)
	}

	alias := f.Name
	if a, err := tagOpt.StringAttr("alias"); err == nil {
		if a != nil {
			alias = *a
		}
	} else {
		return nil, fmt.Errorf("invalid tag value of form.alias, %s.%s: %w", t.Name(), f.Name, err)
	}

	var omitEmpty int8 = 0
	if a, err := tagOpt.BoolAttr("omitEmpty"); err == nil {
		if a != nil {
			if *a {
				omitEmpty = 1
			} else {
				omitEmpty = -1
			}
		}
	} else {
		return nil, fmt.Errorf("invalid tag value of form.omitEmpty, %s.%s: %w", t.Name(), f.Name, err)
	}

	canonicalAlias := alias
	if parentAlias != "" {
		canonicalAlias = parentAlias + "." + alias
	}
	fm := &FieldMeta{
		Typ:            f.Type,
		Name:           f.Name,
		Alias:          alias,
		CanonicalAlias: canonicalAlias,
		Anonymous:      f.Anonymous,
		Tag:            tagOpt,
		OmitEmpty:      omitEmpty,
	}
	return fm, nil
}

func containsAlias(infos []*StructMeta, alias string) bool {
	for _, info := range infos {
		if info.GetField(alias) != nil {
			return true
		}
	}
	return false
}
